{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 第2章: 类型和函数\n",
    "\n",
    "## 类型是干什么用的？\n",
    "\n",
    "- 在计算机最底层，处理的都是没有任何附加结构的字节(byte)，而类型系统在这个基础上提供了抽象。\n",
    "- 一门语言的类型系统会深切地影响这门语言的使用者思考和编写代码的方式。而Haskell的类型系统则允许程序员以非常抽象的层次思考，并写出简洁、高效、健壮的代码。\n",
    "\n",
    "## Haskell的类型系统\n",
    "\n",
    "- Haskell的类型有三个有趣的方面，\n",
    "  - 强类型(strong)\n",
    "  - 静态(static)\n",
    "  - 可以通过自动推导(automatically inferred)得出\n",
    "\n",
    "### 强类型\n",
    "\n",
    "- 保证调用过程类型的匹配正确，如不允许将整数当函数使用，或者将一个字符串传给一个只接受整数参数的函数，遵守类型规则的表达式被称为是“类型正确的”（well typed），而不遵守类型规则、会引起类型错误的表达式被称为是“类型不正确的”（ill typed）。\n",
    "- 不会自动地将值从一个类型转换到另一个类型，这在别的语言是常见的，比如C语言里面if里面的表达式会默认把0，1分别转为false和true.\n",
    "\n",
    "### 静态类型\n",
    "\n",
    "- 静态类型系统指的是，编译器可以在编译期知道每个值和表达式的类型。Haskell编译器或解释器会察觉出类型不正确的表达式，并拒绝这些表达式的执行:\n",
    "\n",
    "  ```\n",
    "  Prelude> True && \"False\"\n",
    "  \n",
    "  <interactive>:2:9:\n",
    "      Couldn't match expected type `Bool' with actual type `[Char]'\n",
    "      In the second argument of `(&&)', namely `\"False\"'\n",
    "      In the expression: True && \"False\"\n",
    "      In an equation for `it': it = True && \"False\"\n",
    "  ```\n",
    "\n",
    "- 静态类型有时候会让某种有用代码的编写变得困难。在 Python 这类语言里， duck typing 非常流行， 只要两个对象的行为足够相似，那么就可以在它们之间进行互换。 幸运的是， Haskell 提供的 typeclass 机制以一种安全、方便、实用的方式提供了大部分动态类型的优点。Haskell 也提供了一部分对全动态类型（truly dynamic types）编程的支持，尽管用起来没有专门支持这种功能的语言那么方便。\n",
    "\n",
    "- Haskell 对强类型和静态类型的双重支持使得程序不可能发生运行时类型错误，这也有助于捕捉那些轻微但难以发现的小错误，作为代价，在编程的时候就要付出更多的努力[译注：比如纠正类型错误和编写类型签名]。Haskell 社区有一种说法，一旦程序编译通过，那么这个程序的正确性就会比用其他语言来写要好得多。（一种更现实的说法是，Haskell 程序的小错误一般都很少。）\n",
    "\n",
    "- 使用动态类型语言编写的程序，常常需要通过大量的测试来预防类型错误的发生，然而，测试通常很难做到巨细无遗：一些常见的任务，比如重构，非常容易引入一些测试没覆盖到的新类型错误。\n",
    "\n",
    "- 另一方面，在 Haskell 里，编译器负责检查类型错误：编译通过的 Haskell 程序是不可能带有类型错误的。而重构 Haskell 程序通常只是移动一些代码块，编译，修复编译错误，并重复以上步骤直到编译无错为止。\n",
    "\n",
    "### 类型推导\n",
    "\n",
    "- 关于类型系统，最后要说的是，Haskell 编译器可以自动推断出程序中几乎所有表达式的类型[注：有时候要提供一些信息，帮助编译器理解程序代码]。这个过程被称为类型推导（type inference）。\n",
    "\n",
    "  虽然 Haskell 允许我们显式地为任何值指定类型，但类型推导使得这种工作通常是可选的，而不是非做不可的事。\n",
    "\n",
    "## 正确理解类型系统\n",
    "\n",
    "- 静态、强类型检查使得 Haskell 更安全，而类型推导则让它更精炼、简洁。这样得出的的结果是，比起其他流行的静态语言，Haskell 要来得更安全，而比起其他流行的动态语言， Haskell 的表现力又更胜一筹。\n",
    "\n",
    "- 修复编译时的类型错误刚开始会让人觉得增加了不必要的工作量，但是，换个角度来看，这不过是提前完成了调试工作：编译器在处理程序时，会将代码中的逻辑错误一一展示出来，而不是一声不吭，任由代码在运行时出错。\n",
    "\n",
    "- 更进一步来说，因为 Haskell 里值和函数的类型都可以通过自动推导得出，所以 Haskell 程序既可以获得静态类型带来的所有好处，而又不必像传统的静态类型语言那样，忙于添加各种各样的类型签名[译注：比如 C 语言的函数原型声明] ——\n",
    "\n",
    "  在其他语言里，类型系统为编译器服务；而在 Haskell 里，类型系统为你服务。唯一的要求是，你需要学习如何在类型系统提供的框架下工作。\n",
    "\n",
    "##  一些常用的类型\n",
    "\n",
    "- `Char`,\n",
    "\n",
    "- `Bool`,\n",
    "\n",
    "- `Int`:带符号的定长（fixed-width）整数。这个值的准确范围由机器决定：在 32 位机器里， `Int` 为 32 位宽，在 64 位机器里， `Int` 为 64 位宽。Haskell 保证 `Int` 的宽度不少于 28 位。（数值类型还可以是 8 位、16 位，等等，也可以是带符号和无符号的，以后会介绍。）\n",
    "\n",
    "- `Integer`:不限长度的带符号整数。 `Integer` 并不像 `Int` 那么常用，因为它们需要更多的内存和更大的计算量。另一方面，对 `Integer` 的计算不会造成溢出，因此使用 `Integer` 的计算结果更可靠。\n",
    "\n",
    "- `Double`:用于表示浮点数。长度由机器决定，通常是 64 位。（Haskell 也有 `Float` 类型，但是并不推荐使用，因为编译器都是针对 `Double` 来进行优化的，而 `Float` 类型值的计算要慢得多。）\n",
    "\n",
    "- 在前面的章节里，我们已经见到过 `::` 符号。除了用来表示类型之外，它还可以用于进行*类型签名*。比如说， `exp :: T` 就是向 Haskell 表示， `exp` 的类型是 `T` ，而 `:: T` 就是表达式 `exp` 的类型签名。如果一个表达式没有显式地指名类型的话，那么它的类型就通过自动推导来决定：\n",
    "\n",
    "  ```\n",
    "  Prelude> :type 'a'\n",
    "  'a' :: Char\n",
    "  \n",
    "  Prelude> 'a'            -- 自动推导\n",
    "  'a'\n",
    "  \n",
    "  Prelude> 'a' :: Char    -- 显式签名\n",
    "  'a'\n",
    "  ```\n",
    "\n",
    "## 调用函数\n",
    "\n",
    "- 函数的调用形式可以不加括号，函数的优先级比表达式高，多个函数一起使用时需要加括号消除二义性\n",
    "\n",
    "## 复合数据结构: 列表和元祖\n",
    "\n",
    "- 复合类型通过其他类型构建得出。列表和元组是 Haskell 中最常用的复合数据类型。\n",
    "\n",
    "  在前面介绍字符串的时候，我们就已经见到过列表类型了： `String` 是 `[Char]` 的别名，而 `[Char]` 则表示由 `Char` 类型组成的列表。\n",
    "\n",
    "- `head`和`tail`可以从列表取头，尾元素,并且值得注意的是，列表可以存放任意类型，`head`,`tail`也可以取任意类型，所以我们称列表为类型多态(polymorphic)的，当需要编写带有多态类型的代码时，需要使用类型变量。这些类型变量以小写字母开头，作为一个占位符，最终被一个具体的类型替换:\n",
    "\n",
    "  ```\n",
    "  ghci>:t head\n",
    "  head :: [a] -> a\n",
    "  ghci>:t []\n",
    "  [] :: [a]\n",
    "  ```\n",
    "\n",
    "- 当需要一个带有具体类型的列表时，就需要用一个具体的类型去替换类型变量。比如说， `[Int]` 表示一个包含 `Int` 类型值的列表，它用 `Int` 类型替换了类型变量 `a` 。又比如， `[MyPersonalType]` 表示一个包含 `MyPersonalType` 类型值的列表，它用 `MyPersonalType` 替换了类型变量 `a` 。\n",
    "\n",
    "  这种替换还还可以递归地进行： `[[Int]]` 是一个包含 `[Int]` 类型值的列表，而 `[Int]` 又是一个包含 `Int` 类型值的列表。以下例子展示了一个包含 `Bool` 类型的列表的列表：\n",
    "\n",
    "  ```\n",
    "  Prelude> :type [[True], [False, False]]\n",
    "  [[True], [False, False]] :: [[Bool]]\n",
    "  ```\n",
    "\n",
    "- 假设现在要用一个数据结构，分别保存一本书的出版年份 —— 一个整数，以及这本书的书名 —— 一个字符串。很明显，列表不能保存这样的信息，因为列表只能接受类型相同的值。这时，我们就需要使用元组：\n",
    "\n",
    "  ```\n",
    "  Prelude> (1964, \"Labyrinths\")\n",
    "  (1964,\"Labyrinths\")\n",
    "  ```\n",
    "\n",
    "  元组和列表非常不同，它们的两个属性刚刚相反：列表可以任意长，且只能包含类型相同的值；元组的长度是固定的，但可以包含不同类型的值。\n",
    "\n",
    "- 元组的两边用括号包围，元素之间用逗号分割。元组的类型信息也使用同样的格式：\n",
    "\n",
    "  ```\n",
    "  Prelude> :type (True, \"hello\")\n",
    "  (True, \"hello\") :: (Bool, [Char])\n",
    "  \n",
    "  Prelude> (4, ['a', 'm'], (16, True))\n",
    "  (4,\"am\",(16,True))\n",
    "  ```\n",
    "\n",
    "  Haskell 有一个特殊的类型 `()` ，这种类型只有一个值 `()` ，它的作用相当于包含零个元素的元组，类似于 C 语言中的 `void` ：\n",
    "\n",
    "  ```\n",
    "  Prelude> :t ()\n",
    "  () :: ()\n",
    "  ```\n",
    "\n",
    "- 通常用元组中元素的数量作为称呼元组的前缀，比如“2-元组”用于称呼包含两个元素的元组，“5-元组”用于称呼包含五个元素的元组，诸如此类。Haskell 不能创建 1-元组，因为 Haskell 没有相应的创建 1-元组的语法（notion）。另外，在实际编程中，元组的元素太多会让代码变得混乱，因此元组通常只包含几个元素。只有元组中的数量、位置和类型都完全相同，这两个元组的类型才是相同的。\n",
    "\n",
    "- 元组通常用于以下两个地方：\n",
    "\n",
    "  - 如果一个函数需要返回多个值，那么可以将这些值都包装到一个元组中，然后返回元组作为函数的值。\n",
    "  - 当需要使用定长容器，但又没有必要使用自定义类型的时候，就可以使用元组来对值进行包装。\n",
    "\n",
    "## 处理列表和元组的函数\n",
    "\n",
    "- `take`,`drop`，接受两个参数，一个数字n和一个列表l。\n",
    "\n",
    "  - `take` 返回一个包含 `l` 前 `n` 个元素的列表：\n",
    "\n",
    "    ```\n",
    "    Prelude> take 2 [1, 2, 3, 4, 5]\n",
    "    [1,2]\n",
    "    ```\n",
    "\n",
    "    `drop` 则返回一个包含 `l` 丢弃了前 `n` 个元素之后，剩余元素的列表：\n",
    "\n",
    "    ```\n",
    "    Prelude> drop 2 [1, 2, 3, 4, 5]\n",
    "    [3,4,5]\n",
    "    ```\n",
    "\n",
    "- 函数 `fst` 和 `snd` 接受一个元组作为参数，返回该元组的第一个元素和第二个元素：\n",
    "\n",
    "  ```\n",
    "  Prelude> fst (1, 'a')\n",
    "  1\n",
    "  \n",
    "  Prelude> snd (1, 'a')\n",
    "  'a'\n",
    "  ```\n",
    "\n",
    "## 将表达式传给函数\n",
    "\n",
    "- Haskell 的函数应用是左关联的。比如说，表达式 `a b c d` 等同于 `(((a b) c) d)` 。要将一个表达式用作另一个表达式的参数，那么就必须显式地使用括号来包围它，这样编译器才会知道我们的真正意思：\n",
    "\n",
    "  ```\n",
    "  Prelude> head (drop 4 \"azety\")\n",
    "  'y'\n",
    "  ```\n",
    "\n",
    "  `drop 4 \"azety\"` 这个表达式被一对括号显式地包围，作为参数传入 `head` 函数。\n",
    "\n",
    "  如果将括号移走，那么编译器就会认为我们试图将三个参数传给 `head` 函数，于是它引发一个错误：\n",
    "\n",
    "  ```\n",
    "  Prelude> head drop 4 \"azety\"\n",
    "  \n",
    "  <interactive>:26:6:\n",
    "      Couldn't match expected type `[t1 -> t2 -> t0]'\n",
    "      with actual type `Int -> [a0] -> [a0]'\n",
    "      In the first argument of `head', namely `drop'\n",
    "      In the expression: head drop 4 \"azety\"\n",
    "      In an equation for `it': it = head drop 4 \"azety\"\n",
    "  ```\n",
    "\n",
    "## 函数类型\n",
    "\n",
    "- 使用`:type`（缩写`:t`）命令可以查看函数的类型\n",
    "\n",
    "- 符号`->`可以读作`映射到`\n",
    "\n",
    "## 纯度\n",
    "\n",
    "- *副作用*指的是，函数的行为受系统的全局状态所影响。\n",
    "\n",
    "  举个命令式语言的例子：假设有某个函数，它读取并返回某个全局变量，如果程序中的其他代码可以修改这个全局变量的话，那么这个函数的返回值就取决于这个全局变量在某一时刻的值。我们说这个函数带有副作用，尽管它并不亲自修改全局变量。\n",
    "\n",
    "  副作用本质上是函数的一种不可见的（invisible）输入或输出。Haskell 的函数在默认情况下都是无副作用的：函数的结果只取决于显式传入的参数。\n",
    "\n",
    "  我们将带副作用的函数称为“不纯（impure）函数”，而将不带副作用的函数称为“纯（pure）函数”。\n",
    "\n",
    "  从类型签名可以看出一个 Haskell 函数是否带有副作用 —— 不纯函数的类型签名都以 `IO` 开头：\n",
    "\n",
    "  ```\n",
    "  Prelude> :type readFile\n",
    "  readFile :: FilePath -> IO String\n",
    "  ```\n",
    "\n",
    "## Haskell 源码，以及简单函数的定义\n",
    "\n",
    "- 因为 ghci 只支持 Haskell 特性的一个非常受限的子集，因此，尽管可以在 ghci 里面定义函数，但那里并不是编写函数最适当的环境。更关键的是， ghci 里面定义函数的语法和 Haskell 源码里定义函数的语法并不相同。综上所述，我们选择将代码写在源码文件里。\n",
    "\n",
    "- **编写源码**：\n",
    "\n",
    "  Haskell 源码通常以 *.hs* 作为后缀。我们创建一个 `add.hs` 文件，并将以下定义添加到文件中：\n",
    "\n",
    "  ```\n",
    "  -- file: ch02/add.hs\n",
    "  add a b = a + b\n",
    "  ```\n",
    "\n",
    "- **导入：**\n",
    "\n",
    "  将 `add.hs` 保存之后，就可以在 ghci 里通过 `:load` 命令（缩写为 `:l` ）载入它，接着就可以像使用其他函数一样，调用 `add` 函数了：\n",
    "\n",
    "  ```\n",
    "  Prelude> :load add.hs\n",
    "  [1 of 1] Compiling Main             ( add.hs, interpreted )\n",
    "  Ok, modules loaded: Main.\n",
    "  \n",
    "  *Main> add 1 2  -- 包载入成功之后 ghci 的提示符会发生变化\n",
    "  3\n",
    "  ```\n",
    "\n",
    "- Haskell 不使用 `return` 关键字来返回函数值：因为一个函数就是一个单独的表达式（expression），而不是一组陈述（statement），求值表达式所得的结果就是函数的返回值。（实际上，Haskell 有一个名为 `return` 的函数，但它和命令式语言里的 `return` 不是同一回事。）\n",
    "\n",
    "\n",
    "\n",
    "## 变量\n",
    "\n",
    "- 在 Haskell 里，可以使用变量来赋予表达式名字：一旦变量绑定了（也即是，关联起）某个表达式，那么这个变量的值就不会改变 —— 我们总能用这个变量来指代它所关联的表达式，并且每次都会得到同样的结果。\n",
    "- 即变量只不过是某个表达式的符号引用，且一旦绑定就不应变化。\n",
    "\n",
    "\n",
    "\n",
    "## 条件求值\n",
    "\n",
    "- `if` 关键字引入了一个带有三个部分的表达式：\n",
    "\n",
    "  - 跟在 `if` 之后的是一个 `Bool` 类型的表达式，它是 `if` 的条件部分。\n",
    "  - 跟在 `then` 关键字之后的是另一个表达式，这个表达式在条件部分的值为 `True` 时被执行。\n",
    "  - 跟在 `else` 关键字之后的又是另一个表达式，这个表达式在条件部分的值为 `False` 时被执行。\n",
    "\n",
    "- 记住，Haskell 是一门以表达式为主导（expression-oriented）的语言。在命令式语言中，代码由陈述（statement）而不是表达式组成，因此在省略 `if` 语句的 `else` 分支的情况下，程序仍是有意义的。但是，当代码由表达式组成时，一个缺少 `else` 分支的 `if` 语句，在条件部分为 `False` 时，是没有办法给出一个结果的，当然这个 `else` 分支也不会有任何类型，因此，省略 `else` 分支对于 Haskell 是无意义的，编译器也不会允许这么做。\n",
    "\n",
    "- 这块的知识点其实比较丰富，还介绍了严格求值，惰性求值，递归，但是基本是面向新手的，所以基本忽略了,值得注意的是懒惰求值使用了名为`thunk`的东西去保留表达式，需要记一下这个概念，后续应该需要深入了解：\n",
    "\n",
    "  这一节介绍了三个重要的知识点：\n",
    "\n",
    "  - 可以通过代换（substitution）和重写（rewriting）去了解 Haskell 求值表达式的方式。\n",
    "  - 惰性求值可以延迟计算直到真正需要一个值为止，并且在求值时，也只执行可以确立出（establish）值的那部分表达式。[译注：比如之前提到的， `(||)` 的左操作对象的值为 `True` 时，就无需对右操作对象估值的情况。]\n",
    "  - 函数的返回值可能是一个块（一个被延迟计算的表达式）(thunk)。\n",
    "\n",
    "\n",
    "\n",
    "## Haskell 里的多态\n",
    "\n",
    "- 之前的列表中就提到过，列表是类型多态的，如果想要取出列表的最后一个元素，那么可以使用last函数，而last函数也不会介意列表里面装的是什么类型，其奥秘在类型签名里就可以体现:\n",
    "\n",
    "  ```\n",
    "  Prelude> :type last\n",
    "  last :: [a] -> a\n",
    "  ```\n",
    "\n",
    "  这个类型签名可以读作“ `last` 接受一个列表，这个列表里的所有元素的类型都为 `a` ，并返回一个类型为 `a` 的元素作为返回值”，其中 `a` 是类型变量。\n",
    "\n",
    "- 如果函数的类型签名里包含类型变量，那么就表示这个函数的某些参数可以是任意类型，我们称这些函数是多态的\n",
    "\n",
    "- 这种类型的多态被称为*参数*多态。可以用一个类比来帮助理解这个名字：就像函数的参数可以被绑定到一个实际的值一样，Haskell 的类型也可以带有参数，并且在稍后可以将这些参数绑定到其它实际的类型上。\n",
    "\n",
    "- 当看见一个参数化类型（parameterized type）时，这表示代码并不在乎实际的类型是什么。另外，我们还可以给出一个更强的陈述：没有办法知道参数化类型的实际类型是什么，也不能操作这种类型的值；不能创建这种类型的值，也不能对这种类型的值进行探查（inspect）。参数化类型唯一能做的事，就是作为一个完全抽象的“黑箱”而存在。\n",
    "\n",
    "- 参数多态是 Haskell 支持的多态中最明显的一个。Haskell 的参数多态直接影响了 Java 和 C# 等语言的泛型（generic）功能的设计。Java 泛型中的类型变量和 Haskell 的参数化类型非常相似。而 C++ 的模板也和参数多态相去不远。\n",
    "\n",
    "- 为了弄清楚 Haskell 的多态和其他语言的多态之间的区别，以下是一些被流行语言所使用的多态形式，这些形式的多态都没有在 Haskell 里出现：\n",
    "\n",
    "  - 在主流的面向对象语言中，*子类*多态是应用得最广泛的一种。C++ 和 Java 的继承机制实现了子类多态，使得子类可以修改或扩展父类所定义的行为。Haskell 不是面向对象语言，因此它没有提供子类多态。\n",
    "\n",
    "  - 另一个常见的多态形式是*强制*多态（coercion polymorphism），它允许值在类型之间进行隐式的转换。很多语言都提供了对强制多态的某种形式的支持，其中一个例子就是：自动将整数类型值转换成浮点数类型值。既然 Haskell 坚决反对自动类型转换，那么这种多态自然也不会出现在 Haskell 里面。\n",
    "\n",
    "### 对多态函数进行推理\n",
    "\n",
    "前面的《函数类型》小节介绍过，可以通过查看函数的类型签名来了解函数的行为。这种方法同样适用于对多态类型进行推理。\n",
    "\n",
    "以 `fst` 函数为例子：\n",
    "\n",
    "```\n",
    "Prelude> :type fst\n",
    "fst :: (a, b) -> a\n",
    "```\n",
    "\n",
    "首先，函数签名包含两个类型变量 `a` 和 `b` ，表明元组可以包含不同类型的值。\n",
    "\n",
    "其次， `fst` 函数的结果值的类型为 `a` 。前面提到过，参数多态没有办法知道输入参数的实际类型，并且它也没有足够的信息构造一个 `a` 类型的值，当然，它也不可以将 `a` 转换为 `b` 。因此，这个函数唯一合法的行为，就是返回元组的第一个元素。\n",
    "\n",
    "\n",
    "\n",
    "### 延伸阅读\n",
    "\n",
    "前一节所说的 `fst` 函数的类型推导行为背后隐藏着非常高深的数学知识，并且可以延伸出一系列复杂的多态函数。有兴趣的话，可以参考 Philip Wadler 的 [Theorems for free](https://ttic.uchicago.edu/~dreyer/course/papers/wadler.pdf)  论文。\n",
    "\n",
    "\n",
    "\n",
    "## 多参函数的类型\n",
    "\n",
    "- 截至目前为止，我们已经见到过一些函数，比如 `take` ，它们接受一个以上的参数：\n",
    "\n",
    "```\n",
    "Prelude> :type take\n",
    "take :: Int -> [a] -> [a]\n",
    "```\n",
    "\n",
    "​\t通过类型签名可以看到， `take` 函数和一个 `Int` 值以及两个列表有关。类型签名中的 `->` 符号是右关联的： Haskell 从右到左地串联起这些箭头，使用括号可以清晰地标示这个类型签名是怎样被解释的：\n",
    "\n",
    "```\n",
    "-- file: ch02/Take.hs\n",
    "take :: Int -> ([a] -> [a])\n",
    "```\n",
    "\n",
    "从这个新的类型签名可以看出， `take` 函数实际上只接受一个 `Int` 类型的参数，并返回另一个函数，这个新函数接受一个列表作为参数，并返回一个同类型的列表作为这个函数的结果。\n",
    "\n",
    "- 以上的说明都是正确的，但要说清楚隐藏在这种变换背后的重要性并不容易，在《部分函数应用和柯里化》一节，我们会再次回到这个主题上。目前来说，可以简单地将类型签名中最后一个 `->` 右边的类型看作是函数结果的类型，而将前面的其他类型看作是函数参数的类型。\n",
    "\n",
    "- 了解了这些之后，现在可以为前面定义的 `myDrop` 函数编写类型签名了：\n",
    "\n",
    "```\n",
    "myDrop :: Int -> [a] -> [a]\n",
    "```\n",
    "\n",
    "## 练习\n",
    "\n",
    "1. Haskell 提供了一个标准函数, last :: [a] -> a, 其返回一个列表的最后一个元素．单就从他的类型看, 这个函数拥有的合理行为是怎样的(忽略崩溃和无限循环)？ 有哪些少数的事情这个函数显然不能做到?\n",
    "   - 无法处理无限列表\n",
    "2. 写一个函数 lastButOne, 返回列表倒数第二个元素.\n",
    "   - 递归终止条件判断下，列表长度等于2时返回head\n",
    "3. 加载 lastButOne 函数到 ghci, 并且尝试在不同长度的列表上测试. 当你传入的列表过短时候，发生了什么?\n",
    "   - 异常或自己的特殊处理或逻辑错误导致的意外结果\n",
    "\n",
    "\n",
    "\n",
    "## 为什么要对纯度斤斤计较\n",
    "\n",
    "- 很少有语言像 Haskell 那样，默认使用纯函数。这个选择不仅意义深远，而且至关重要。\n",
    "\n",
    "- 因为纯函数的值只取决于输入的参数，所以通常只要看看函数的名字，还有它的类型签名，就能大概知道函数是干什么用的。\n",
    "\n",
    "  以 `not` 函数为例子：\n",
    "\n",
    "```\n",
    "Prelude> :type not\t\t\n",
    "not :: Bool -> Bool\n",
    "```\n",
    "\n",
    "- 即使抛开函数名不说，单单函数签名就极大地限制了这个函数可能有的合法行为：\n",
    "  - 函数要么返回 True ，要么返回 False\n",
    "  - 函数直接将输入参数当作返回值返回\n",
    "  - 函数对它的输入值求反\n",
    "\n",
    "- 除此之外，我们还能肯定，这个函数不会干以下这些事情：读取文件，访问网络，或者返回当前时间。\n",
    "- 纯度减轻了理解一个函数所需的工作量。一个纯函数的行为并不取决于全局变量、数据库的内容或者网络连接状态。纯代码（pure code）从一开始就是模块化的：每个函数都是自包容的，并且都带有定义良好的接口。\n",
    "\n",
    "- 将纯函数作为默认的另一个不太明显的好处是，它使得与*不纯*代码之间的交互变得简单。一种常见的 Haskell 风格就是，将带有副作用的代码和不带副作用的代码分开处理。在这种情况下，不纯函数需要尽可能地简单，而复杂的任务则交给纯函数去做。\n",
    "\n",
    "- 软件的大部分风险，都来自于与外部世界进行交互：它需要程序去应付错误的、不完整的数据，并且处理恶意的攻击，诸如此类。Haskell 的类型系统明确地告诉我们，哪一部分的代码带有副作用，让我们可以对这部分代码添加适当的保护措施。通过这种将不纯函数隔离、并尽可能简单化的编程风格，程序的漏洞将变得非常少。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Haskell",
   "language": "haskell",
   "name": "haskell"
  },
  "language_info": {
   "codemirror_mode": "ihaskell",
   "file_extension": ".hs",
   "name": "haskell",
   "pygments_lexer": "Haskell",
   "version": "8.6.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
